/*
 * This file is part of the OWASP Proxy, a free intercepting proxy library.
 * Copyright (C) 2008-2010 Rogan Dawes <rogan@dawes.za.net>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to:
 * The Free Software Foundation, Inc., 
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

package org.owasp.proxy.io;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

/**
 * Implements an input stream filter for compressing data in the "deflate"
 * compression format.
 * 
 * @since 1.6
 * @author David R Tribble (david@tribble.com)
 * 
 * @see DeflaterOutputStream
 * @see InflaterOutputStream
 * @see InflaterInputStream
 */

public class DeflaterInputStream extends FilterInputStream {
	/** Compressor for this stream. */
	protected final Deflater def;

	/** Input buffer for reading compressed data. */
	protected final byte[] buf;

	/** Temporary read buffer. */
	private byte[] rbuf = new byte[1];

	/** Default compressor is used. */
	private boolean usesDefaultDeflater = false;

	/** End of the underlying input stream has been reached. */
	private boolean reachEOF = false;

	/**
	 * Check to make sure that this stream has not been closed.
	 */
	private void ensureOpen() throws IOException {
		if (in == null) {
			throw new IOException("Stream closed");
		}
	}

	/**
	 * Creates a new input stream with a default compressor and buffer size.
	 * 
	 * @param in
	 *            input stream to read the uncompressed data to
	 * @throws NullPointerException
	 *             if {@code in} is null
	 */
	public DeflaterInputStream(InputStream in) {
		this(in, new Deflater());
		usesDefaultDeflater = true;
	}

	/**
	 * Creates a new input stream with the specified compressor and a default
	 * buffer size.
	 * 
	 * @param in
	 *            input stream to read the uncompressed data to
	 * @param defl
	 *            compressor ("deflater") for this stream
	 * @throws NullPointerException
	 *             if {@code in} or {@code defl} is null
	 */
	public DeflaterInputStream(InputStream in, Deflater defl) {
		this(in, defl, 512);
	}

	/**
	 * Creates a new input stream with the specified compressor and buffer size.
	 * 
	 * @param in
	 *            input stream to read the uncompressed data to
	 * @param defl
	 *            compressor ("deflater") for this stream
	 * @param bufLen
	 *            compression buffer size
	 * @throws IllegalArgumentException
	 *             if {@code bufLen} is <= 0
	 * @throws NullPointerException
	 *             if {@code in} or {@code defl} is null
	 */
	public DeflaterInputStream(InputStream in, Deflater defl, int bufLen) {
		super(in);

		// Sanity checks
		if (in == null)
			throw new NullPointerException("Null input");
		if (defl == null)
			throw new NullPointerException("Null deflater");
		if (bufLen < 1)
			throw new IllegalArgumentException("Buffer size < 1");

		// Initialize
		def = defl;
		buf = new byte[bufLen];
	}

	/**
	 * Closes this input stream and its underlying input stream, discarding any
	 * pending uncompressed data.
	 * 
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public void close() throws IOException {
		if (in != null) {
			try {
				// Clean up
				if (usesDefaultDeflater) {
					def.end();
				}

				in.close();
			} finally {
				in = null;
			}
		}
	}

	/**
	 * Reads a single byte of compressed data from the input stream. This method
	 * will block until some input can be read and compressed.
	 * 
	 * @return a single byte of compressed data, or -1 if the end of the
	 *         uncompressed input stream is reached
	 * @throws IOException
	 *             if an I/O error occurs or if this stream is already closed
	 */
	public int read() throws IOException {
		// Read a single byte of compressed data
		int len = read(rbuf, 0, 1);
		if (len <= 0)
			return -1;
		return (rbuf[0] & 0xFF);
	}

	/**
	 * Reads compressed data into a byte array. This method will block until
	 * some input can be read and compressed.
	 * 
	 * @param b
	 *            buffer into which the data is read
	 * @param off
	 *            starting offset of the data within {@code b}
	 * @param len
	 *            maximum number of compressed bytes to read into {@code b}
	 * @return the actual number of bytes read, or -1 if the end of the
	 *         uncompressed input stream is reached
	 * @throws IndexOutOfBoundsException
	 *             if {@code len} > {@code b.length - off}
	 * @throws IOException
	 *             if an I/O error occurs or if this input stream is already
	 *             closed
	 */
	public int read(byte[] b, int off, int len) throws IOException {
		// Sanity checks
		ensureOpen();
		if (b == null) {
			throw new NullPointerException("Null buffer for read");
		} else if (off < 0 || len < 0 || len > b.length - off) {
			throw new IndexOutOfBoundsException();
		} else if (len == 0) {
			return 0;
		}

		// Read and compress (deflate) input data bytes
		int cnt = 0;
		while (len > 0 && !def.finished()) {
			int n;

			// Read data from the input stream
			if (def.needsInput()) {
				n = in.read(buf, 0, buf.length);
				if (n < 0) {
					// End of the input stream reached
					def.finish();
				} else if (n > 0) {
					def.setInput(buf, 0, n);
				}
			}

			// Compress the input data, filling the read buffer
			n = def.deflate(b, off, len);
			cnt += n;
			off += n;
			len -= n;
		}
		if (cnt == 0 && def.finished()) {
			reachEOF = true;
			cnt = -1;
		}

		return cnt;
	}

	/**
	 * Skips over and discards data from the input stream. This method may block
	 * until the specified number of bytes are read and skipped. <em>Note:</em>
	 * While {@code n} is given as a {@code long}, the maximum number of bytes
	 * which can be skipped is {@code Integer.MAX_VALUE}.
	 * 
	 * @param n
	 *            number of bytes to be skipped
	 * @return the actual number of bytes skipped
	 * @throws IOException
	 *             if an I/O error occurs or if this stream is already closed
	 */
	public long skip(long n) throws IOException {
		if (n < 0) {
			throw new IllegalArgumentException("negative skip length");
		}
		ensureOpen();

		// Skip bytes by repeatedly decompressing small blocks
		if (rbuf.length < 512)
			rbuf = new byte[512];

		int total = (int) Math.min(n, Integer.MAX_VALUE);
		long cnt = 0;
		while (total > 0) {
			// Read a small block of uncompressed bytes
			int len = read(rbuf, 0,
					(total <= rbuf.length ? total : rbuf.length));

			if (len < 0) {
				break;
			}
			cnt += len;
			total -= len;
		}
		return cnt;
	}

	/**
	 * Returns 0 after EOF has been reached, otherwise always return 1.
	 * <p>
	 * Programs should not count on this method to return the actual number of
	 * bytes that could be read without blocking
	 * 
	 * @return zero after the end of the underlying input stream has been
	 *         reached, otherwise always returns 1
	 * @throws IOException
	 *             if an I/O error occurs or if this stream is already closed
	 */
	public int available() throws IOException {
		ensureOpen();
		if (reachEOF) {
			return 0;
		}
		return 1;
	}

	/**
	 * Always returns {@code false} because this input stream does not support
	 * the {@link #mark mark()} and {@link #reset reset()} methods.
	 * 
	 * @return false, always
	 */
	public boolean markSupported() {
		return false;
	}

	/**
	 * <i>This operation is not supported</i>.
	 * 
	 * @param limit
	 *            maximum bytes that can be read before invalidating the
	 *            position marker
	 */
	public void mark(int limit) {
		// Operation not supported
	}

	/**
	 * <i>This operation is not supported</i>.
	 * 
	 * @throws IOException
	 *             always thrown
	 */
	public void reset() throws IOException {
		throw new IOException("mark/reset not supported");
	}

}
